/**
 * Copyright (C) 2008 Stefan Maassen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.indisopht.guice.groovy;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.codehaus.groovy.control.CompilationFailedException;

import com.google.inject.CreationException;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.spi.Message;

/**
 * Integrates Guice with Groovy. Requires a constant or collection bound to
 * {@link de.indisopht.guice.groovy.GroovyClasspath}.
 * 
 * @author Stefan Maassen
 */
public final class GroovyIntegration {

    private static final GroovyIntegration instance = new GroovyIntegration();

    @Inject(optional = true)
    private ClasspathContainer classpath;
    
    @Inject(optional = true)
    @GroovyRecompile
    private Boolean recompileCode = null;
    
    @Inject(optional = true)
    @GroovyCodeBase
    private String codeBase = null;
    
    @Inject(optional = true)
    @GroovyResolve
    private boolean resolve=false;
    
    private GroovyClassLoader groovyLoader;
    
    List<Message> errorMessages = new ArrayList<Message>();

    private GroovyIntegration() {
    }

    /**
     * Creates a provider for MyClass using the given name pointing to a file or
     * by parsing the given string as a class. <br>
     * <br>
     * Example usage:<br>
     * 
     * <pre>
     * &lt;b&gt;Binding a groovy class to a java class&lt;/b&gt;
     * <code>
     * binder.bindConstant().annotatedWith(GroovyClasspath.class).to(System.getProperty(&quot;groovy.classpath&quot;));
     * bind(MyClass.class).to(fromGroovy(MyClass.class, &quot;com.mypackage....&quot;));
     * </code>
     * &lt;b&gt;Binding grovvy code inside a string to a java class&lt;/b&gt;
     * <code>
     * bind(MyClass.class).to(fromGroovy(MyClass.class, &quot;import a.b.c; class B extends MyClass { }&quot;));
     * </code>
     * &lt;b&gt;Binding a grovvy script inside a string&lt;/b&gt;
     * <code>
     * binder.bind(Script.class).toProvider(GroovyIntegration.fromGroovy(Script.class, &quot;println('groovy hello'); 'groovy'&quot;));
     * </code>
     * &lt;b&gt;Binding a grovvy script from a file&lt;/b&gt;
     * <code>
     * binder.bindConstant().annotatedWith(GroovyClasspath.class).to(URLClassLoader.getSystemResource(&quot;TestScript.groovy&quot;).getPath());
     * binder.bind(Script.class).toProvider(GroovyIntegration.fromGroovy(Script.class, &quot;TestScript&quot;));
     * </code>
     * &lt;b&gt;Binding security permissions to scripts from .policy files&lt;/b&gt;
     * <code>
     * binder.bindConstant().annotatedWith(GroovyCodeBase.class).to("/secure/codeBase"));
     * </code>
     * &lt;b&gt;Set resolving behaviour of GroovyClassloader&lt;/b&gt;
     * <code>
     * binder.bindConstant().annotatedWith(GroovyResolve.class).to(Boolean.TRUE);
     * </code>
     * </pre>
     * <pre>
     * &lt;b&gt;NOTE: MyClass must be known at runtime, when binding to Groovy.&lt;/b&gt;
     * </pre>
     */
    public static <T> Provider<T> fromGroovy(Class<T> type, String somethingGroovy) {
        return new GroovyProvider<T>(type, somethingGroovy);
    }

    /**
     * does the main work of the integration
     * 
     * @param <T>
     *            the type of the wanted class
     * @param type
     *            the class
     * @param somethingGroovy
     *            a fully qualified name OR source code
     * @return the wanted class
     */
    @SuppressWarnings("unchecked")
    private <T> Class<T> loadClass(Class<T> type, String somethingGroovy) {
        errorMessages.clear();
        if (groovyLoader == null) {
            /*
             * prepare classloader
             */
            groovyLoader = new GroovyClassLoader(type.getClassLoader());
            if (recompileCode != null) {
                groovyLoader.setShouldRecompile(recompileCode);
            }
            for (String currentPath : classpath) {
                if (currentPath.contains("://")) {
                    try {
                        groovyLoader.addURL(new URL(currentPath));
                    } catch (MalformedURLException e) {
                        errorMessages.add(new Message(e.getMessage()));
                    }
                } else {
                    groovyLoader.addClasspath(currentPath);
                }
            }
        }
        GroovyCodeSource codeSource = null;
        Class<T> result = null;
        /*
         * let groovy try to load the class
         */
        try {
            result = groovyLoader.loadClass(somethingGroovy, true, true, resolve);
        } catch (CompilationFailedException e) {
            errorMessages.add(new Message(e.getMessage()));
        } catch (ClassNotFoundException e) {
            // ignore this exception here
        }
        if (errorMessages.size() > 0) {
            throw new CreationException(errorMessages);
        }
        if (result != null) {
            return result;
        }
        /*
         * let groovy try to parse and compile the given string
         */
        if ((codeBase!=null) && (codeBase.length()>0)) { 
            codeSource=new GroovyCodeSource(somethingGroovy, groovyLoader.generateScriptName(), codeBase);
            try {
                result = groovyLoader.parseClass(codeSource, true);
            } catch (CompilationFailedException e) {
                errorMessages.add(new Message("can't parse [["+somethingGroovy+"]] due to "+e.getMessage()));
            }
        } else {
            /*
             * let groovy parse and compile the given string
             */
            try {
                result = groovyLoader.parseClass(somethingGroovy);
            } catch (CompilationFailedException e) {
                errorMessages.add(new Message("couldn't parse " + somethingGroovy + " due to " + e.getMessage()));
            }
        }
        
        if (result==null) {
            if (errorMessages.size()==0) {
                throw new CreationException(Arrays.asList(new Message("couldn't get anything groovy out of given parameter "+somethingGroovy)));
            } else {
                throw new CreationException(errorMessages);
            }
        }
        /*
         * the end
         */
        return result;
    }
    
    static class GroovyProvider<T> implements Provider<T> {

        @Inject
        Injector injector;
        
        private final Class<T> classToBindTo;
        private final String groovyClass;

        public GroovyProvider(Class<T> classToBindTo, String groovyClass) {
            super();
            this.classToBindTo = classToBindTo;
            this.groovyClass = groovyClass;
        }

        @Override
        public T get() {
            if (instance.classpath == null) {
                injector.injectMembers(instance);
            }
            Class<T> classFromGroovy = instance.loadClass(classToBindTo, groovyClass);
            return injector.getInstance(classFromGroovy);
        }
    }
}